package ch.bfh.ti.barcode.colorQrCode;

import java.util.EnumMap;

import Jama.Matrix;
import ch.bfh.ti.barcode.CodeReader;
import ch.bfh.ti.barcode.exception.DecodeException;
import ch.bfh.ti.barcode.exception.NotDetectedException;

import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.DecodeHintType;
import com.google.zxing.FormatException;
import com.google.zxing.HelperMethodsFromZXing;
import com.google.zxing.LuminanceSource;
import com.google.zxing.NotFoundException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.DecoderResult;
import com.google.zxing.common.DetectorResult;
import com.google.zxing.common.GridSampler;
import com.google.zxing.common.PerspectiveTransform;
import com.google.zxing.qrcode.decoder.Decoder;
import com.google.zxing.qrcode.detector.Detector;

/**
 * This class extracts the data of a Color QR-Code. It does that by separating
 * the colors and converting the three resulting channels into black and white
 * QR-Codes. Those QR-Codes are then being processed by the ZXing-Library.
 * 
 * @author Dominik von Burg
 * 
 */
public class ColorQrCodeReader implements CodeReader {
	private int width = 0;
	private int height = 0;
	private EnumMap<DecodeHintType, Object> hints;
	private Decoder decoder = new Decoder();
	private GridSampler sampler = GridSampler.getInstance();
	private final int detectSensitivity;
	private final int decodeSensitivity;

	/**
	 * Instantiates a new Color QR-Code reader. The with and height of the image
	 * containing the barcode has to be provided ant the instantiation and can
	 * not be changed during runtime.
	 * 
	 * @param width
	 *            The width of the image containing the barcode.
	 * @param height
	 *            The height of the image containing the barcode.
	 * @param detectSensitivity
	 *            Sensitivity for the detection of the barcode edges. The higher
	 *            the value the more colors turn black.
	 * @param decodeSensitivity
	 *            Sensitivity for the decoding of the color channels. The higher
	 *            the value the more colors turn black.
	 */
	public ColorQrCodeReader(int width, int height, int detectSensitivity,
			int decodeSensitivity) {
		this.width = width;
		this.height = height;

		this.detectSensitivity = detectSensitivity;
		this.decodeSensitivity = decodeSensitivity;

		hints = new EnumMap<DecodeHintType, Object>(DecodeHintType.class);

		// True binary transfer is not supported. We convert binary data to this
		// charset and back.
		hints.put(DecodeHintType.CHARACTER_SET, "ISO-8859-1");
		// The barcode will be detected and cropped manually. Nothing to do for
		// the library...
		hints.put(DecodeHintType.PURE_BARCODE, "true");
	}

	/**
	 * This method reads the data of the Color QR-Code from a image. The image
	 * has to be provided in the ARGB format. The data is returned as a byte
	 * array.
	 * 
	 * @param source
	 *            An image with the Color QR-Code. The image has to be in the
	 *            ARGB format.
	 * @return The decoded data extracted from the barcode image.
	 * @throws NotDetectedException
	 *             The barcode could not be found in the image.
	 * @throws DecodeException
	 *             The barcode could be detected but there was an error with the
	 *             decoding.
	 */
	public byte[] decode(int[] source) throws NotDetectedException,
			DecodeException {
		/*
		 * The channels are not summarized for the BW channel. The darkest
		 * channel is taken. This makes the detection of the edged more
		 * reliable.
		 */
		byte[] datal = new byte[source.length];
		int minValue;
		for (int x = 0; x < datal.length; x++) {
			minValue = (source[x] >> 8) & 0xff;
			minValue = Math.min(minValue, source[x] & 0xff);
			minValue = Math.min(minValue, (source[x] >> 16) & 0xff);
			minValue = Math.max(0, minValue);
			datal[x] = (byte) (minValue);
		}

		/*
		 * Convert the data of the BW channel into a BitMatrix. The BitMatrix
		 * only has one bit data for each pixel.
		 */
		LuminanceSource lsource = new SimpleLuminanceSource(width, height,
				datal);
		BinaryBitmap bitmap = new BinaryBitmap(new SimpleBinarizer(lsource,
				detectSensitivity));
		BitMatrix matrix;
		try {
			matrix = bitmap.getBlackMatrix();
		} catch (NotFoundException e) {
			throw new NotDetectedException(
					"Could not generate the black matrix!");
		}

		/*
		 * Detect the edges in the BW BitMatrix. Read out the colors at the edge
		 * position from the source image.
		 */
		Detector detector = new Detector(matrix);
		
		DetectorResult detectorResult;
		try {
			detectorResult = detector.detect();
		} catch (NotFoundException e) {
			throw new NotDetectedException("Could not find the edges!");
		} catch (FormatException e) {
			throw new NotDetectedException("Barcode is in a wrong format!");
		}

		int topLeftR = source[(int) detectorResult.getPoints()[1].getX()
				+ (width * (int) detectorResult.getPoints()[1].getY())] & 0xff;
		int topLeftG = (source[(int) detectorResult.getPoints()[1].getX()
				+ (width * (int) detectorResult.getPoints()[1].getY())] >> 8) & 0xff;
		int topLeftB = (source[(int) detectorResult.getPoints()[1].getX()
				+ (width * (int) detectorResult.getPoints()[1].getY())] >> 16) & 0xff;

		int topRightR = source[(int) detectorResult.getPoints()[2].getX()
				+ (width * (int) detectorResult.getPoints()[2].getY())] & 0xff;
		int topRightG = (source[(int) detectorResult.getPoints()[2].getX()
				+ (width * (int) detectorResult.getPoints()[2].getY())] >> 8) & 0xff;
		int topRightB = (source[(int) detectorResult.getPoints()[2].getX()
				+ (width * (int) detectorResult.getPoints()[2].getY())] >> 16) & 0xff;

		int bottomLeftR = source[(int) detectorResult.getPoints()[0].getX()
				+ (width * (int) detectorResult.getPoints()[0].getY())] & 0xff;
		int bottomLeftG = (source[(int) detectorResult.getPoints()[0].getX()
				+ (width * (int) detectorResult.getPoints()[0].getY())] >> 8) & 0xff;
		int bottomLeftB = (source[(int) detectorResult.getPoints()[0].getX()
				+ (width * (int) detectorResult.getPoints()[0].getY())] >> 16) & 0xff;

		/*
		 * Create a matrix with the color values and invert it. The matrix has
		 * to be inverted and multiplied with 255.
		 */
		double[][] edgeMatrix = new double[][] {
				{ bottomLeftR, topLeftR, topRightR },
				{ bottomLeftG, topLeftG, topRightG },
				{ bottomLeftB, topLeftB, topRightB } };

		Matrix mc = new Matrix(edgeMatrix);
		// Taking the inverse of a matrix can fail if the matrix is singular!
		try {
			mc = mc.inverse();
			mc = mc.times(255);

			edgeMatrix = mc.getArray();
		} catch (RuntimeException e) {
			edgeMatrix = new double[][] { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
		}

		/*
		 * Correct the source and split it into three channels with the
		 * luminance values.
		 */
		byte[] datar = new byte[width * height];
		byte[] datag = new byte[width * height];
		byte[] datab = new byte[width * height];

		double sum = 0;
		for (int x = 0; x < width * height; x++) {
			for (int y = 0; y < 3; y++) {
				/*
				 * Calculate the dot product. No method doing that found in the
				 * Jama library.
				 */
				sum = 0;
				for (int z = 0; z < 3; z++)
					sum += edgeMatrix[y][z]
							* (double) ((source[x] >> (z * 8)) & 0xff);

				// make sure the values are under 255 and over 0
				sum = Math.max(sum, 0);
				sum = Math.min(sum, 255);

				switch (y) {
				case 0:
					datab[x] = (byte) sum;
					break;
				case 1:
					datag[x] = (byte) sum;
					break;
				case 2:
					datar[x] = (byte) sum;
					break;
				}
			}
		}

		/*
		 * Calculate the size, the angle and the dimension with some methods
		 * from ZXing. This values are necessary to cut out the colored parts.
		 */
		float moduleSize = HelperMethodsFromZXing.calculateModuleSize(
				detectorResult.getPoints()[1], detectorResult.getPoints()[2],
				detectorResult.getPoints()[0], matrix);
		if (moduleSize < 1.0f) {
			throw new DecodeException("Could not find the size of the module!");
		}
		int dimension;
		try {
			dimension = HelperMethodsFromZXing.computeDimension(
					detectorResult.getPoints()[1],
					detectorResult.getPoints()[2],
					detectorResult.getPoints()[0], moduleSize);
		} catch (NotFoundException e) {
			throw new DecodeException(
					"Could not find the dimension of the module!");
		}

		PerspectiveTransform transform = HelperMethodsFromZXing
				.createTransform(detectorResult.getPoints()[1], detectorResult
						.getPoints()[2], detectorResult.getPoints()[0],
						detectorResult.getPoints().length < 4 ? null
								: detectorResult.getPoints()[3], dimension);

		// red
		byte[] result;
		try {
			result = decodeColorChannel(datar, dimension, transform);
		} catch (NotFoundException e) {
			throw new DecodeException(
					"Could not decode red channel. (not found)");
		} catch (FormatException e) {
			throw new DecodeException("Could not decode red channel. (format)");
		} catch (ChecksumException e) {
			throw new DecodeException(
					"Could not decode red channel. (checksum)");
		}

		byte[] concatResult = new byte[(result.length-1) * 3];
		System.arraycopy(result, 1, concatResult, 0, result.length-1);

		// green
		try {
			result = decodeColorChannel(datag, dimension, transform);
		} catch (NotFoundException e) {
			throw new DecodeException(
					"Could not decode green channel. (not found)");
		} catch (FormatException e) {
			throw new DecodeException(
					"Could not decode green channel. (format)");
		} catch (ChecksumException e) {
			throw new DecodeException(
					"Could not decode green channel. (checksum)");
		}

		System.arraycopy(result, 1, concatResult, result.length-1, result.length-1);

		// blue
		try {
			result = decodeColorChannel(datab, dimension, transform);
		} catch (NotFoundException e) {
			throw new DecodeException(
					"Could not decode blue channel. (not found)");
		} catch (FormatException e) {
			throw new DecodeException("Could not decode blue channel. (format)");
		} catch (ChecksumException e) {
			throw new DecodeException(
					"Could not decode blue channel. (checksum)");
		}

		System.arraycopy(result, 1, concatResult, (result.length-1) * 2,
				result.length-1);

		return concatResult;
	}

	private byte[] decodeColorChannel(byte[] pixels, int dimension,
			PerspectiveTransform transform) throws NotFoundException,
			FormatException, ChecksumException, DecodeException {
		LuminanceSource lsource = new SimpleLuminanceSource(width, height,
				pixels);
		BitMatrix matrix = new SimpleBinarizer(lsource, decodeSensitivity)
				.getBlackMatrix();

		matrix = sampler.sampleGrid(matrix, dimension, dimension, transform);

		DecoderResult rawResult = decoder.decode(matrix);
		return rawResult.getByteSegments().get(0);
	}
}